// Copyright 2018 The Draco Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

#ifndef DRACO_COMPRESSION_ATTRIBUTES_POINT_D_VECTOR_H_
#define DRACO_COMPRESSION_ATTRIBUTES_POINT_D_VECTOR_H_

#include <cstring>
#include <memory>
#include <vector>
#include "draco/core/macros.h"

namespace draco {

// The main class of this file is PointDVector providing an interface similar to
// std::vector<PointD> for arbitrary number of dimensions (without a template
// argument). PointDVectorIterator is a random access iterator, which allows for
// compatibility with existing algorithms. PseudoPointD provides for a view on
// the individual items in a contiguous block of memory, which is compatible
// with the swap function and is returned by a dereference of
// PointDVectorIterator. Swap functions provide for compatibility/specialization
// that allows these classes to work with currently utilized STL functions.

// This class allows for swap functionality from the RandomIterator
// It seems problematic to bring this inside PointDVector due to templating.
template <typename internal_t>
class PseudoPointD {
  public:
    PseudoPointD(internal_t *mem, internal_t dimension)
        : mem_(mem), dimension_(dimension) {}

    // Specifically copies referenced memory
    void swap(PseudoPointD &other) noexcept {
        for (auto dim = 0; dim < dimension_; dim += 1)
            std::swap(mem_[dim], other.mem_[dim]);
    }

    PseudoPointD(const PseudoPointD &other)
        : mem_(other.mem_), dimension_(other.dimension_) {}

    const internal_t &operator[](const size_t &n) const {
        DRACO_DCHECK_LT(n, dimension_);
        return mem_[n];
    }
    internal_t &operator[](const size_t &n) {
        DRACO_DCHECK_LT(n, dimension_);
        return mem_[n];
    }

    bool operator==(const PseudoPointD &other) const {
        for (auto dim = 0; dim < dimension_; dim += 1)
            if (mem_[dim] != other.mem_[dim])
                return false;
        return true;
    }
    bool operator!=(const PseudoPointD &other) const {
        return !this->operator==(other);
    }

  private:
    internal_t *const mem_;
    const internal_t dimension_;
};

// It seems problematic to bring this inside PointDVector due to templating.
template <typename internal_t>
void swap(draco::PseudoPointD<internal_t> &&a,
          draco::PseudoPointD<internal_t> &&b) noexcept {
    a.swap(b);
};
template <typename internal_t>
void swap(draco::PseudoPointD<internal_t> &a,
          draco::PseudoPointD<internal_t> &b) noexcept {
    a.swap(b);
};

template <typename internal_t>
class PointDVector {
  public:
    PointDVector(const uint32_t n_items, const uint32_t dimensionality)
        : n_items_(n_items),
          dimensionality_(dimensionality),
          item_size_bytes_(dimensionality * sizeof(internal_t)),
          data_(n_items * dimensionality),
          data0_(data_.data()) {}
    // random access iterator
    class PointDVectorIterator
        : public std::iterator<std::random_access_iterator_tag, size_t, size_t> {
        friend class PointDVector;

      public:
        // std::iter_swap is called inside of std::partition and needs this
        // specialized support
        PseudoPointD<internal_t> operator*() const {
            return PseudoPointD<internal_t>(vec_->data0_ + item_ * dimensionality_,
                                            dimensionality_);
        }
        const PointDVectorIterator &operator++() {
            item_ += 1;
            return *this;
        }
        const PointDVectorIterator &operator--() {
            item_ -= 1;
            return *this;
        }
        PointDVectorIterator operator++(int32_t) {
            PointDVectorIterator copy(*this);
            item_ += 1;
            return copy;
        }
        PointDVectorIterator operator--(int32_t) {
            PointDVectorIterator copy(*this);
            item_ -= 1;
            return copy;
        }
        PointDVectorIterator &operator=(const PointDVectorIterator &other) {
            this->item_ = other.item_;
            return *this;
        }

        bool operator==(const PointDVectorIterator &ref) const {
            return item_ == ref.item_;
        }
        bool operator!=(const PointDVectorIterator &ref) const {
            return item_ != ref.item_;
        }
        bool operator<(const PointDVectorIterator &ref) const {
            return item_ < ref.item_;
        }
        bool operator>(const PointDVectorIterator &ref) const {
            return item_ > ref.item_;
        }
        bool operator<=(const PointDVectorIterator &ref) const {
            return item_ <= ref.item_;
        }
        bool operator>=(const PointDVectorIterator &ref) const {
            return item_ >= ref.item_;
        }

        PointDVectorIterator operator+(const int32_t &add) const {
            PointDVectorIterator copy(vec_, item_ + add);
            return copy;
        }
        PointDVectorIterator &operator+=(const int32_t &add) {
            item_ += add;
            return *this;
        }
        PointDVectorIterator operator-(const int32_t &sub) const {
            PointDVectorIterator copy(vec_, item_ - sub);
            return copy;
        }
        size_t operator-(const PointDVectorIterator &sub) const {
            return (item_ - sub.item_);
        }

        PointDVectorIterator &operator-=(const int32_t &sub) {
            item_ -= sub;
            return *this;
        }

        internal_t *operator[](const size_t &n) const {
            return vec_->data0_ + (item_ + n) * dimensionality_;
        }

      protected:
        explicit PointDVectorIterator(PointDVector *vec, size_t start_item)
            : item_(start_item), vec_(vec), dimensionality_(vec->dimensionality_) {}

      private:
        size_t item_;  // this counts the item that should be referenced.
        PointDVector *const vec_;        // the thing that we're iterating on
        const uint32_t dimensionality_;  // local copy from vec_
    };

    PointDVectorIterator begin() {
        return PointDVectorIterator(this, 0);
    }
    PointDVectorIterator end() {
        return PointDVectorIterator(this, n_items_);
    }

    // operator[] allows for unprotected user-side usage of operator[] on the
    // return value AS IF it were a natively indexable type like Point3*
    internal_t *operator[](const uint32_t index) {
        DRACO_DCHECK_LT(index, n_items_);
        return data0_ + index * dimensionality_;
    }
    const internal_t *operator[](const uint32_t index) const {
        DRACO_DCHECK_LT(index, n_items_);
        return data0_ + index * dimensionality_;
    }

    uint32_t size() const {
        return n_items_;
    }
    size_t GetBufferSize() const {
        return data_.size();
    }

    // copy a single contiguous 'item' from one PointDVector into this one.
    void CopyItem(const PointDVector &source, const internal_t source_index,
                  const internal_t destination_index) {
        DRACO_DCHECK(&source != this ||
                     (&source == this && source_index != destination_index));
        DRACO_DCHECK_LT(destination_index, n_items_);
        DRACO_DCHECK_LT(source_index, source.n_items_);

        // DRACO_DCHECK_EQ(source.n_items_, n_items_); // not technically necessary
        DRACO_DCHECK_EQ(source.dimensionality_, dimensionality_);

        const internal_t *ref = source[source_index];
        internal_t *const dest = this->operator[](destination_index);
        std::memcpy(dest, ref, item_size_bytes_);
    }

    // Copy data directly off of an attribute buffer interleaved into internal
    // memory.
    void CopyAttribute(
        // The dimensionality of the attribute being integrated
        const internal_t attribute_dimensionality,
        // The offset in dimensions to insert this attribute.
        const internal_t offset_dimensionality, const internal_t index,
        // The direct pointer to the data
        const void *const attribute_item_data) {
        // chunk copy
        const size_t copy_size = sizeof(internal_t) * attribute_dimensionality;

        // a multiply and add can be optimized away with an iterator
        std::memcpy(data0_ + index * dimensionality_ + offset_dimensionality,
                    attribute_item_data, copy_size);
    }
    // Copy data off of a contiguous buffer interleaved into internal memory
    void CopyAttribute(
        // The dimensionality of the attribute being integrated
        const internal_t attribute_dimensionality,
        // The offset in dimensions to insert this attribute.
        const internal_t offset_dimensionality,
        const internal_t *const attribute_mem) {
        DRACO_DCHECK_LT(offset_dimensionality,
                        dimensionality_ - attribute_dimensionality);
        // degenerate case block copy the whole buffer.
        if (dimensionality_ == attribute_dimensionality) {
            DRACO_DCHECK_EQ(offset_dimensionality, 0);
            const size_t copy_size =
                sizeof(internal_t) * attribute_dimensionality * n_items_;
            std::memcpy(data0_, attribute_mem, copy_size);
        } else {  // chunk copy
            const size_t copy_size = sizeof(internal_t) * attribute_dimensionality;
            internal_t *internal_data;
            const internal_t *attribute_data;
            internal_t item;
            for (internal_data = data0_ + offset_dimensionality,
                    attribute_data = attribute_mem, item = 0;
                    item < n_items_; internal_data += dimensionality_,
                    attribute_data += attribute_dimensionality, item += 1) {
                std::memcpy(internal_data, attribute_data, copy_size);
            }
        }
    }

  private:
    // internal parameters.
    const uint32_t n_items_;
    const uint32_t dimensionality_;  // The dimension of the points in the buffer
    const uint32_t item_size_bytes_;
    std::vector<internal_t> data_;  // contiguously stored data.  Never resized.
    internal_t *const data0_;       // raw pointer to base data.
};

}  // namespace draco

#endif  // DRACO_COMPRESSION_ATTRIBUTES_POINT_D_VECTOR_H_
